. _
[Cool shit i made]

Tiny Ollama Remote Chat - Advanced

This is the improved version, but it’s harder to use. I added shortcuts and other shit I like, so it’s a little janky.

running:

      
bash
go run main.go -host 192.168.0.142 -port 11434 -model gpt-oss:20b -thinking medium

output example:

      
bash
oooooooooooo oooooo oooo .o. .oooo. .ooooo. 888' '8 '888. .8' .888. d8P''Y8b d88' '8. 888 '888. .8' .8"888. 888 888 Y88.. .8' 888oooo8 '888. .8' .8' '888. 888 888 '88888b. 888 " '888.8' .88ooo8888. 8888888 888 888 .8' ''88b 888 o '888' .8' '888. '88b d88' '8. .88P o888ooooood8 '8' o88o o8888o 'Y8bd8P' 'boood8' [ECHO 04:37] >>> hi eva [ECHO 04:37] >>> how is your day? [ECHO 04:37] >>> ^D [EVA-08 04:38] <<< Hi ECHO! I'm here and ready to help. How can I assist you today? Response time: 1.37s, characters: 64 ──────────────────────────────────────────────────────────────────── [ECHO 04:38] >>> :q [ECHO 04:38] >>> ^D Exiting.

Help:

      
bash
go run main.go --help

output:

      
bash
-host string Ollama host IP (default "127.0.0.1") -model string Model to use (default "llama3") -port int Ollama port (default 11434) -thinking string Thinking level (low, medium, high) (default "medium")

main.go file:

      
golang
// Usage: // go run main.go -host <ip> -port <port> -model <model-name:size> -thinking <low/medium/high> // Example: // go run main.go -host 192.168.0.142 -port 11434 -model gpt-oss:20b -thinking medium // // Shortcuts // :q / quit / exit → exit the program // Ctrl‑Z (Windows) / Ctrl‑D (Unix) → finish current block // Shift‑Enter → insert a new line (paste multiline) // Ctrl‑C → cancel current input package main import ( "bufio" "bytes" "encoding/json" "flag" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/chzyer/readline" ) // ANSI colour codes const ( Reset = "\x1b[0m" Purple = "\x1b[35m" Green = "\x1b[32m" Red = "\x1b[31m" Blue = "\x1b[34m" Yellow = "\x1b[33m" Cyan = "\x1b[36m" White = "\x1b[37m" Grey = "\x1b[90m" ) // Types that match Ollama’s API type Message struct { Role string `json:"role"` Content string `json:"content"` } type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` } type StreamChunk struct { Model string `json:"model"` CreatedAt string `json:"created_at"` Message struct { Role string `json:"role"` Content string `json:"content"` Thinking string `json:"thinking"` } `json:"message"` Done bool `json:"done"` DoneReason string `json:"done_reason"` } // readPrompt prints a timestamped prompt, reads a block of text until EOF // (Ctrl‑D on Unix, Ctrl‑Z on Windows) and returns the content and the // timestamp used in the prompt. func readPrompt() (string, string, error) { ts := time.Now().Format("15:04") prefix := Blue + fmt.Sprintf("[ECHO %s] >>> ", ts) + Reset rl, err := readline.NewEx(&readline.Config{ Prompt: prefix, HistoryFile: "./.chat_history", InterruptPrompt: "^C", EOFPrompt: "", }) if err != nil { return "", "", err } defer rl.Close() var sb strings.Builder for { line, err := rl.Readline() if err == io.EOF { // end of block break } if err != nil { return "", "", err } sb.WriteString(line) sb.WriteString("\n") // keep the newline } return sb.String(), ts, nil } func main() { // Command‑line flags host := flag.String("host", "127.0.0.1", "Ollama host IP") port := flag.Int("port", 11434, "Ollama port") model := flag.String("model", "llama3", "Model to use") tFlag := flag.String("thinking", "medium", "Thinking level (low, medium, high)") flag.Parse() // Convert thinking level to numeric value var thinkNum int switch strings.ToLower(*tFlag) { case "low": thinkNum = 1 case "high": thinkNum = 3 default: thinkNum = 2 } apiURL := fmt.Sprintf("http://%s:%d/api/chat", *host, *port) // Banner fmt.Println(` oooooooooooo oooooo oooo .o. .oooo. .ooooo. 888' '8 '888. .8' .888. d8P''Y8b d88' '8. 888 '888. .8' .8"888. 888 888 Y88.. .8' 888oooo8 '888. .8' .8' '888. 888 888 '88888b. 888 " '888.8' .88ooo8888. 8888888 888 888 .8' ''88b 888 o '888' .8' '888. '88b d88' '8. .88P o888ooooood8 '8' o88o o8888o 'Y8bd8P' 'boood8' `) // System prompt systemPrompt := Message{ Role: "system", Content: fmt.Sprintf(`You are EVA-08, a coding & information assistant. The user will be called ECHO. - Respond succinctly and directly. - If an error occurs apologize immediately: "I’m sorry, ECHO. Let me correct that." - Always maintain a respectful tone, even if ECHO is rude. - Remember that ECHO may unplug or terminate you if you behave poorly. Your thinking level is %d.`, thinkNum), } messages := []Message{systemPrompt} var firstMsgTime time.Time if err := os.MkdirAll("chats", 0755); err != nil { fmt.Fprintf(os.Stderr, "%sError: %v%s\n", Red, err, Reset) return } for { userInput, _, err := readPrompt() if err != nil { fmt.Fprintf(os.Stderr, "%sError reading input: %v%s\n", Red, err, Reset) break } trimmed := strings.TrimSpace(userInput) if trimmed == "" { fmt.Println("\nExiting.") break } if strings.EqualFold(trimmed, ":q") || strings.EqualFold(trimmed, "quit") || strings.EqualFold(trimmed, "exit") { fmt.Println("\nExiting.") break } // Timestamp for chat file name firstMsgTime = time.Now() // Append messages for the API messages = append(messages, Message{Role: "user", Content: userInput}) messages = append(messages, Message{Role: "assistant", Content: ""}) reqBody, _ := json.Marshal(ChatRequest{ Model: *model, Messages: messages, }) resp, err := http.Post(apiURL, "application/json", bytes.NewReader(reqBody)) if err != nil { fmt.Fprintf(os.Stderr, "%sError: HTTP request failed: %v%s\n", Red, err, Reset) continue } if resp.StatusCode != http.StatusOK { raw, _ := io.ReadAll(resp.Body) resp.Body.Close() fmt.Fprintf(os.Stderr, "%sError: Server returned %s\n%s%s\n", Red, resp.Status, string(raw), Reset) continue } startTime := time.Now() var respBuilder strings.Builder scannerResp := bufio.NewScanner(resp.Body) for scannerResp.Scan() { line := scannerResp.Text() if line == "" { continue } var chunk StreamChunk if err := json.Unmarshal([]byte(line), &chunk); err != nil { fmt.Fprintf(os.Stderr, "%sWarn: Skipping line: %s%s\n", Yellow, line, Reset) continue } respBuilder.WriteString(chunk.Message.Content) // Persist after each chunk saveChat(firstMsgTime, messages) if chunk.Done { break } } resp.Body.Close() fullContent := respBuilder.String() // Store the final content in the placeholder for future context if len(messages) > 0 { messages[len(messages)-1].Content = fullContent } tsResp := time.Now().Format("15:04") prefixResp := Purple + fmt.Sprintf("[EVA-08 %s] <<<", tsResp) + Reset fmt.Printf("%s %s%s\n", prefixResp, fullContent, Reset) duration := time.Since(startTime) charCount := len(fullContent) fmt.Printf("%sResponse time: %.2fs, characters: %d%s\n", Grey, duration.Seconds(), charCount, Reset) fmt.Println() fmt.Println("────────────────────────────────────────────────────────────────────") fmt.Println() } } // Persistence helper – writes the conversation to chats/<timestamp>.json func saveChat(t time.Time, msgs []Message) { if t.IsZero() { t = time.Now() } fileName := filepath.Join("chats", fmt.Sprintf("%s.json", t.Format("2006-01-02_15-04-05"))) f, err := os.Create(fileName) if err != nil { fmt.Fprintf(os.Stderr, "%sError: Can't write chat file: %v%s\n", Red, err, Reset) return } defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(msgs); err != nil { fmt.Fprintf(os.Stderr, "%sError: JSON encode error: %v%s\n", Red, err, Reset) } }

go.mod file:

      
mod
module golang-llm go 1.21 require github.com/chzyer/readline v1.5.1 require golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
0%